/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the EternalTech source code.

 The EternalTech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The EternalTech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the EternalTech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// r_lightSurface.cpp - light and shadow surface interaction lists
//


#include "r_local.h"


struct lightSurfaces_t {
	int						numShadows;
	int						maxShadows;
	int						firstShadow;
	drawSurface_t **		shadows;

	int						numInteractions;
	int						maxInteractions;
	int						firstInteraction;
	drawSurface_t **		interactions;
};

static lightSurfaces_t		r_lightSurfaces;


/*
 ==================
 R_AddShadowSurface
 ==================
*/
static void R_AddShadowSurface (drawLight_t *dl, dsType_t type, dsGeometry_t *geometry, entity_t *entity, material_t *material, uint bits){

	drawSurface_t	*ds;

	// Add a new draw surface
	if (r_lightSurfaces.numShadows == r_lightSurfaces.maxShadows){
		Com_DPrintf(S_COLOR_YELLOW "R_AddShadowSurface: overflow\n");
		return;
	}

	r_lightSurfaces.shadows[r_lightSurfaces.numShadows++] = ds = (drawSurface_t *)R_FrameAlloc(sizeof(drawSurface_t));

	// Fill it in
	ds->type = type;
	ds->geometry = geometry;

	ds->entity = entity;
	ds->material = material;

	ds->sort = (material->sort << DS_SHIFT_SORT) | (entity->index << DS_SHIFT_ENTITY) | (material->index << DS_SHIFT_MATERIAL) | type;
	ds->bits = bits;
}

/*
 ==================
 R_AddInteractionSurface
 ==================
*/
static void R_AddInteractionSurface (drawLight_t *dl, dsType_t type, dsGeometry_t *geometry, entity_t *entity, material_t *material, uint bits){

	drawSurface_t	*ds;

	// Add a new draw surface
	if (r_lightSurfaces.numInteractions == r_lightSurfaces.maxInteractions){
		Com_DPrintf(S_COLOR_YELLOW "R_AddInteractionSurface: overflow\n");
		return;
	}

	r_lightSurfaces.interactions[r_lightSurfaces.numInteractions++] = ds = (drawSurface_t *)R_FrameAlloc(sizeof(drawSurface_t));

	// Fill it in
	ds->type = type;
	ds->geometry = geometry;

	ds->entity = entity;
	ds->material = material;

	ds->sort = (material->sort << DS_SHIFT_SORT) | (entity->index << DS_SHIFT_ENTITY) | (material->index << DS_SHIFT_MATERIAL) | type;
	ds->bits = bits;
}

/*
 ==================
 R_AddLightSurface

 FIXME: make sure SURF_PLANEBACK is used correct
 ==================
*/
static void R_AddLightSurface (drawLight_t *dl, surface_t *surface, material_t *material, entity_t *entity, const glqVec3 &lightOrigin, const glqVec3 &lightDirection, int cullBits, bool addShadow, bool addInteraction){

	// Cull face
	if (dl->material->lightType == LT_GENERIC){
		if (!r_skipFaceCulling->integerValue && surface->plane && material->deform == DFRM_NONE){
			if (dl->light->l.type == RL_DIRECTIONAL){
				if (material->cullType == CT_FRONT_SIDED){
					if (!(surface->flags & SURF_PLANEBACK)){
						if (lightDirection.Dot(surface->plane->Normal()) >= 0.0f)
							return;
					}
				}
				else if (material->cullType == CT_BACK_SIDED){
					if (lightDirection.Dot(surface->plane->Normal()) <= 0.0f)
						return;
				}
			}
			else {
				if (material->cullType == CT_FRONT_SIDED){
					if (!(surface->flags & SURF_PLANEBACK)){
						if (surface->plane->PointOnSide(lightOrigin) != PLANESIDE_FRONT)
							return;
					}
				}
				else if (material->cullType == CT_BACK_SIDED){
					if (surface->plane->PointOnSide(lightOrigin) != PLANESIDE_BACK)
						return;
				}
			}
		}
	}

	// Cull bounds
	if (cullBits != CULL_IN){
		if (entity == rg.scene->worldEntity){
			if (R_LightCullBounds(&dl->light->data, surface->bounds, cullBits) == CULL_OUT)
				return;
		}
		else {
			if (R_LightCullLocalBounds(&dl->light->data, surface->bounds, entity->e.origin, entity->e.axis, cullBits) == CULL_OUT)
				return;
		}
	}

	// Add the draw surface
	if (addShadow)
		R_AddShadowSurface(dl, DS_SURFACE, surface, entity, material, 63);

	if (addInteraction)
		R_AddInteractionSurface(dl, DS_SURFACE, surface, entity, material, 63);
}


/*
 ==============================================================================

 WORLD LIGHT SURFACES

 ==============================================================================
*/


/*
 ==================
 
 ==================
*/
static void R_LoadPrecachedLightSurfaces (drawLight_t *dl, bool lightInFrustum, bool castShadows, int skipFlag){

	lightNode_t	*lightNode;
	surface_t	*surface;
	bool		addShadow, addInteraction;
	int			i;

	// Check all the nodes
	for (lightNode = dl->light->data.nodes; lightNode; lightNode = lightNode->next){
		// Check visibility
		if (lightNode->leaf->viewCount != rg.viewCount){
			if (lightInFrustum || !castShadows)
				return;
		}

		// Add all the surfaces
		for (i = 0; i < lightNode->numSurfaces; i++){
			surface = lightNode->surfaces[i];

			if (surface->checkCount == rg.checkCount)
				continue;		// Already added this surface from another leaf
			surface->checkCount = rg.checkCount;

			// Check visibility
			if (surface->viewCount != rg.viewCount){
				// Not visible, but may still cast visible shadows
				if (lightInFrustum || !castShadows)
					return;
			}

			// Determine if we should add the shadow
			if (castShadows)
				addShadow = !(surface->texInfo->material->flags & MF_NOSHADOWS);
			else
				addShadow = false;

			// Determine if we should add the interaction
			if (surface->viewCount == rg.viewCount)
				addInteraction = !(surface->texInfo->material->flags & skipFlag);
			else
				addInteraction = false;

			// Add the draw surface
			if (addShadow)
				R_AddShadowSurface(dl, DS_SURFACE, surface, rg.scene->worldEntity, surface->texInfo->material, 63);

			if (addInteraction)
				R_AddInteractionSurface(dl, DS_SURFACE, surface, rg.scene->worldEntity, surface->texInfo->material, 63);
		}
	}

	rg.pc.precachedShadows += (r_lightSurfaces.numShadows - r_lightSurfaces.firstShadow);
	rg.pc.precachedInteractions += (r_lightSurfaces.numInteractions - r_lightSurfaces.firstInteraction);
}

/*
 ==================
 R_RecursiveWorldLightNode
 ==================
*/
static void R_RecursiveWorldLightNode (drawLight_t *dl, node_t *node, bool lightInFrustum, bool castShadows, int skipFlag, int cullBits){

	leaf_t		*leaf;
	surface_t	*surface, **mark;
	bool		addShadow, addInteraction;
	int			i;

	while (1){
		// Check for solid content
		if (node->contents == CONTENT_SOLID)
			return;

		// Check visibility
		if (node->viewCount != rg.viewCount){
			// Not visible, but may still cast visible shadows
			if (lightInFrustum || !castShadows)
				return;
		}

		// Cull
		if (cullBits != CULL_IN){
			cullBits = R_LightCullBounds(&dl->light->data, node->bounds, cullBits);

			if (cullBits == CULL_OUT)
				return;
		}

		if (node->contents < 0)
			break;		// Leaf node

		// Recurse down the children, front side first
		R_RecursiveWorldLightNode(dl, node->children[0], lightInFrustum, castShadows, skipFlag, cullBits);

		// Tail recurse
		node = node->children[1];
	}

	// Leaf node
	leaf = (leaf_t *)node;

	if (!leaf->numMarkSurfaces)
		return;

	// If it has a single surface, no need to cull again
	if (leaf->numMarkSurfaces == 1)
		cullBits = CULL_IN;

	// Add all the surfaces
	for (i = 0, mark = leaf->firstMarkSurface; i < leaf->numMarkSurfaces; i++, mark++){
		surface = *mark;

		if (surface->checkCount == rg.checkCount)
			continue;		// Already added this surface from another leaf
		surface->checkCount = rg.checkCount;

		if (surface->texInfo->material->spectrum != dl->material->spectrum)
			continue;		// Not illuminated by this light

		// Check visibility
		if (surface->viewCount != rg.viewCount){
			// Not visible, but may still cast visible shadows
			if (lightInFrustum || !castShadows)
				continue;
		}

		// Determine if we should add the shadow
		if (castShadows)
			addShadow = !(surface->texInfo->material->flags & MF_NOSHADOWS);
		else
			addShadow = false;

		// Determine if we should add the interaction
		if (surface->viewCount == rg.viewCount)
			addInteraction = !(surface->texInfo->material->flags & skipFlag);
		else
			addInteraction = false;

		// Check if there's nothing to be added
		if (!addShadow && !addInteraction)
			continue;

		// Add the surface
		R_AddLightSurface(dl, surface, surface->texInfo->material, rg.scene->worldEntity, dl->light->data.origin, dl->light->data.direction, cullBits, addShadow, addInteraction);
	}
}

/*
 ==================
 R_AddWorldLightSurfaces
 ==================
*/
static void R_AddWorldLightSurfaces (drawLight_t *dl, bool lightInFrustum, bool castShadows, int skipFlag){

	if (!rg.viewParms.primaryView)
		return;

	// Bump check count
	rg.checkCount++;

	// If we have precached lists of nodes and surfaces
	if (!r_skipLightCache->integerValue && dl->light->data.precached){
		R_LoadPrecachedLightSurfaces(dl, lightInFrustum, castShadows, skipFlag);
		return;
	}

	// Recurse down the BSP tree
	if (r_skipCulling->integerValue)
		R_RecursiveWorldLightNode(dl, rg.world->nodes, lightInFrustum, castShadows, skipFlag, CULL_IN);
	else
		R_RecursiveWorldLightNode(dl, rg.world->nodes, lightInFrustum, castShadows, skipFlag, LIGHT_PLANEBITS);
}


/*
 ==============================================================================

 ENTITY LIGHT SURFACES

 ==============================================================================
*/


/*
 ==================
 
 FIXME: we need a sphere before this can contiue
 ==================
*/
static void R_AddInlineModelLightSurfaces (drawLight_t *dl, entity_t *entity, bool lightInFrustum, bool castShadows, int skipFlag){

	inlineModel_t	*model = (inlineModel_t *)entity->e.model->data;
	surface_t		*surface;
	material_t		*material;
	glqVec3			lightOrigin, lightDirection;
	int				cullBits;
	bool			addShadow, addInteraction;
	int				i;

	// Check visibility
	if (entity->viewCount != rg.viewCount){
		// Not visible, but may still cast visible shadows
		if (lightInFrustum || !castShadows)
			return;
	}

	// Cull
	cullBits = 0;

	// Transform light origin and light direction into local space
	R_WorldPointToLocal(dl->light->data.origin, lightOrigin, entity->e.origin, entity->e.axis);
	R_WorldVectorToLocal(dl->light->data.direction, lightDirection, entity->e.axis);

	// Add all the surfaces
	for (i = 0, surface = model->surfaces; i < model->numSurfaces; i++, surface++){
		// Get the material
		if (entity->e.material)
			material = entity->e.material;
		else
			material = R_RemapMaterialBySkin(surface->texInfo->material, entity->e.skin);

		if (material->spectrum != dl->material->spectrum)
			continue;		// Not illuminated by this light

		// Determine if we should add the shadow
		if (castShadows)
			addShadow = !(material->flags & MF_NOSHADOWS);
		else
			addShadow = false;

		// Determine if we should add the interaction
		if (entity->viewCount == rg.viewCount)
			addInteraction = !(material->flags & skipFlag);
		else
			addInteraction = false;

		// Check if there's nothing to be added
		if (!addShadow && !addInteraction)
			continue;

		// Add the surface
		R_AddLightSurface(dl, surface, material, entity, lightOrigin, lightDirection, cullBits, addShadow, addInteraction);
	}
}

/*
 ==================
 
 FIXME: we need a sphere before this can contiue
 ==================
*/
static void R_AddAliasModelLightSurfaces (drawLight_t *dl, entity_t *entity, bool lightInFrustum, bool castShadows, int skipFlag){

	aliasMdl_t			*alias = (aliasMdl_t *)entity->e.model->data;
	aliasMdlSurface_t	*surface;
	material_t			*material;
	int					cullBits;
	bool				addShadow, addInteraction;
	int					i;

	// Check visibility
	if (entity->viewCount != rg.viewCount){
		// Not visible, but may still cast visible shadows
		if (lightInFrustum || !castShadows)
			return;
	}

	// Cull

	// Add all the surfaces
	for (i = 0, surface = alias->surfaces; i < alias->numSurfaces; i++, surface++){
		// Get the material
		if (entity->e.material)
			material = entity->e.material;
		else
			material = R_RemapMaterialBySkin(surface->materials[i].material, entity->e.skin);

		if (material->spectrum != dl->material->spectrum)
			continue;		// Not illuminated by this light

		// Determine if we should add the shadow
		if (castShadows)
			addShadow = !(material->flags & MF_NOSHADOWS);
		else
			addShadow = false;

		// Determine if we should add the interaction
		if (entity->viewCount == rg.viewCount)
			addInteraction = !(material->flags & skipFlag);
		else
			addInteraction = false;

		// Check if there's nothing to be added
		if (!addShadow && !addInteraction)
			continue;

		// Cull

		// Add the draw surface
		if (addShadow)
			R_AddShadowSurface(dl, DS_MODEL, surface, entity, material, 63);

		if (addInteraction)
			R_AddInteractionSurface(dl, DS_MODEL, surface, entity, material, 63);
	}
}

/*
 ==================
 
 ==================
*/
static void R_AddSpriteModelLightSurfaces (drawLight_t *dl, entity_t *entity, bool lightInFrustum, bool castShadows, int skipFlag){

}

/*
 ==================
 R_AddEntityLightSurfaces
 ==================
*/
static void R_AddEntityLightSurfaces (drawLight_t *dl, bool lightInFrustum, bool castShadows, int skipFlag){

	entity_t	*entity;
	bool		suppressShadows;

	if (r_skipEntities->integerValue)
		return;

	for (entity = rg.scene->activeEntity.next; entity != &rg.scene->activeEntity; entity = entity->next){
		if (entity == rg.scene->worldEntity)
			continue;		// World entity

		if (entity->e.type != RE_MODEL)
			continue;		// Not a model

		// Development tool
		if (r_singleEntity->integerValue != -1){
			if (r_singleEntity->integerValue != entity->index)
				continue;
		}

		// Check for view suppression
		if (!r_skipSuppress->integerValue)
			suppressShadows = !(entity->e.allowShadowInView & rg.viewParms.viewType);
		else
			suppressShadows = false;

		// Add the entity
		if (!entity->e.model || entity->e.model->type == MODEL_BAD)
			continue;

		switch (entity->e.model->type){
		case MODEL_INLINE:
			R_AddInlineModelLightSurfaces(dl, entity, lightInFrustum, castShadows && !suppressShadows && !entity->e.depthHack, skipFlag);
			break;
		case MODEL_MDL:
		case MODEL_MD3:
			R_AddAliasModelLightSurfaces(dl, entity, lightInFrustum, castShadows && !suppressShadows && !entity->e.depthHack, skipFlag);
			break;
		case MODEL_SPRITE:
			R_AddSpriteModelLightSurfaces(dl, entity, lightInFrustum, castShadows && !suppressShadows && !entity->e.depthHack, skipFlag);
			break;
		default:
			Com_Error(false, "R_AddEntityLightSurfaces: bad model type (%i)", entity->e.model->type);
		}
	}
}


// ============================================================================


/*
 ==================
 R_AllocLightSurfaces
 ==================
*/
void R_AllocLightSurfaces (){

	r_lightSurfaces.numShadows = 0;
	r_lightSurfaces.maxShadows = MAX_DRAW_SURFACES << 2;
	r_lightSurfaces.firstShadow = 0;
	r_lightSurfaces.shadows = (drawSurface_t **)R_FrameAlloc(r_lightSurfaces.maxShadows * sizeof(drawSurface_t *));

	r_lightSurfaces.numInteractions = 0;
	r_lightSurfaces.maxInteractions = MAX_DRAW_SURFACES << 2;
	r_lightSurfaces.firstInteraction = 0;
	r_lightSurfaces.interactions = (drawSurface_t **)R_FrameAlloc(r_lightSurfaces.maxInteractions * sizeof(drawSurface_t *));
}

/*
 ==================
 R_GenerateLightSurfaces
 ==================
*/
void R_GenerateLightSurfaces (drawLight_t *dl){

	glqPlane	*plane;
	bool		lightInFrustum;
	bool		castShadows;
	int			skipFlag;
	int			i;

	// Clear
	dl->numShadowSurfaces = 0;
	dl->shadowSurfaces = NULL;

	dl->numInteractionSurfaces = 0;
	dl->interactionSurfaces = NULL;

	// Determine if the light origin is inside the view frustum
	if (dl->light->l.type == RL_DIRECTIONAL)
		lightInFrustum = false;
	else {
		for (i = 0, plane = rg.viewParms.frustum; i < NUM_FRUSTUM_PLANES; i++, plane++){
			if (!(rg.viewParms.planeBits & BIT(i)))
				continue;

			if (plane->PointOnSide(dl->light->data.origin) == PLANESIDE_BACK)
				break;
		}

		if (i == 5)
			lightInFrustum = true;
		else
			lightInFrustum = false;
	}

	// Determine if it casts shadows
	if (dl->light->l.noShadows || (dl->material->flags & MF_NOSHADOWS))
		castShadows = false;
	else {
		if (dl->shadowLOD == -1 || dl->shadowFilter == -1)
			castShadows = false;
		else
			castShadows = true;
	}

	// Determine the interaction skip flag
	if (dl->material->lightType == LT_AMBIENT)
		skipFlag = MF_NOAMBIENT;
	else if (dl->material->lightType == LT_BLEND)
		skipFlag = MF_NOBLEND;
	else if (dl->material->lightType == LT_FOG)
		skipFlag = MF_NOFOG;
	else {
		skipFlag = MF_NOINTERACTIONS;

		if (dl->light->l.type != RL_DIRECTIONAL && dl->light->l.volumeIntensity){
			if (!dl->light->l.volumeShadows && !dl->light->l.volumeInteractions)
				return;

			if (!dl->light->l.volumeInteractions)
				skipFlag = -1;
		}
	}

	// Add the draw surfaces
	R_AddWorldLightSurfaces(dl, lightInFrustum, castShadows, skipFlag);
	R_AddEntityLightSurfaces(dl, lightInFrustum, castShadows, skipFlag);

	// Set up the draw surfaces
	dl->numShadowSurfaces = r_lightSurfaces.numShadows - r_lightSurfaces.firstShadow;
	dl->shadowSurfaces = r_lightSurfaces.shadows + r_lightSurfaces.firstShadow;

	dl->numInteractionSurfaces = r_lightSurfaces.numInteractions - r_lightSurfaces.firstInteraction;
	dl->interactionSurfaces = r_lightSurfaces.interactions + r_lightSurfaces.firstInteraction;

	rg.pc.shadowSurfaces += dl->numShadowSurfaces;
	rg.pc.interactionSurfaces += dl->numInteractionSurfaces;

	R_SetupDrawSurfaces(dl->numShadowSurfaces, dl->shadowSurfaces);
	R_SetupDrawSurfaces(dl->numInteractionSurfaces, dl->interactionSurfaces);

	// The next light rendered in this frame will tack on after this one
	r_lightSurfaces.firstShadow = r_lightSurfaces.numShadows;
	r_lightSurfaces.firstInteraction = r_lightSurfaces.numInteractions;
}